MaĂźtrisez React Testing Library (RTL) avec ce guide complet. Ăcrivez des tests efficaces, maintenables et centrĂ©s sur l'utilisateur pour vos applications React.
React Testing Library : Un guide complet
Dans le paysage actuel du développement web, qui évolue rapidement, il est primordial de garantir la qualité et la fiabilité de vos applications React. React Testing Library (RTL) s'est imposée comme une solution populaire et efficace pour écrire des tests axés sur la perspective de l'utilisateur. Ce guide offre un aperçu complet de RTL, couvrant tout, des concepts fondamentaux aux techniques avancées, pour vous permettre de créer des applications React robustes et maintenables.
Pourquoi choisir React Testing Library ?
Les approches de test traditionnelles reposent souvent sur les détails d'implémentation, ce qui rend les tests fragiles et susceptibles de se briser lors de changements mineurs dans le code. RTL, au contraire, vous encourage à tester vos composants comme le ferait un utilisateur, en se concentrant sur ce que l'utilisateur voit et expérimente. Cette approche offre plusieurs avantages clés :
- Tests centrés sur l'utilisateur : RTL favorise l'écriture de tests qui reflÚtent la perspective de l'utilisateur, garantissant que votre application fonctionne comme prévu du point de vue de l'utilisateur final.
- Réduction de la fragilité des tests : En évitant de tester les détails d'implémentation, les tests RTL sont moins susceptibles de se briser lorsque vous refactorisez votre code, ce qui conduit à des tests plus maintenables et robustes.
- Amélioration de la conception du code : RTL vous encourage à écrire des composants accessibles et faciles à utiliser, ce qui conduit à une meilleure conception globale du code.
- Accent sur l'accessibilité : RTL facilite le test de l'accessibilité de vos composants, garantissant que votre application est utilisable par tous.
- Processus de test simplifié : RTL fournit une API simple et intuitive, ce qui facilite l'écriture et la maintenance des tests.
Mise en place de votre environnement de test
Avant de pouvoir commencer à utiliser RTL, vous devez configurer votre environnement de test. Cela implique généralement l'installation des dépendances nécessaires et la configuration de votre framework de test.
Prérequis
- Node.js et npm (ou yarn) : Assurez-vous que Node.js et npm (ou yarn) sont installés sur votre systÚme. Vous pouvez les télécharger sur le site officiel de Node.js.
- Projet React : Vous devez avoir un projet React existant ou en créer un nouveau en utilisant Create React App ou un outil similaire.
Installation
Installez les paquets suivants en utilisant npm ou yarn :
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Ou, en utilisant yarn :
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Explication des paquets :
- @testing-library/react : La bibliothĂšque principale pour tester les composants React.
- @testing-library/jest-dom : Fournit des matchers Jest personnalisĂ©s pour faire des assertions sur les nĆuds du DOM.
- Jest : Un framework de test JavaScript populaire.
- babel-jest : Un transformateur Jest qui utilise Babel pour compiler votre code.
- @babel/preset-env : Un préréglage Babel qui détermine les plugins et préréglages Babel nécessaires pour supporter vos environnements cibles.
- @babel/preset-react : Un préréglage Babel pour React.
Configuration
Créez un fichier `babel.config.js` à la racine de votre projet avec le contenu suivant :
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Mettez Ă jour votre fichier `package.json` pour inclure un script de test :
{
"scripts": {
"test": "jest"
}
}
Créez un fichier `jest.config.js` à la racine de votre projet pour configurer Jest. Une configuration minimale pourrait ressembler à ceci :
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};
Créez un fichier `src/setupTests.js` avec le contenu suivant. Cela garantit que les matchers DOM de Jest sont disponibles dans tous vos tests :
import '@testing-library/jest-dom/extend-expect';
Ăcrire votre premier test
Commençons par un exemple simple. Supposons que vous ayez un composant React qui affiche un message de salutation :
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Maintenant, écrivons un test pour ce composant :
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('affiche un message de salutation', () => {
render(<Greeting name="World" />);
const greetingElement = screen.getByText(/Hello, World!/i);
expect(greetingElement).toBeInTheDocument();
});
Explication :
- `render` : Cette fonction effectue le rendu du composant dans le DOM.
- `screen` : Cet objet fournit des méthodes pour interroger le DOM.
- `getByText` : Cette méthode trouve un élément par son contenu textuel. L'indicateur `/i` rend la recherche insensible à la casse.
- `expect` : Cette fonction est utilisée pour faire des assertions sur le comportement du composant.
- `toBeInTheDocument` : Ce matcher affirme que l'élément est présent dans le DOM.
Pour exécuter le test, exécutez la commande suivante dans votre terminal :
npm test
Si tout est configuré correctement, le test devrait passer.
Les requĂȘtes courantes de RTL
RTL fournit diverses mĂ©thodes de requĂȘte pour trouver des Ă©lĂ©ments dans le DOM. Ces requĂȘtes sont conçues pour imiter la façon dont les utilisateurs interagissent avec votre application.
`getByRole`
Cette requĂȘte trouve un Ă©lĂ©ment par son rĂŽle ARIA. C'est une bonne pratique d'utiliser `getByRole` chaque fois que possible, car cela favorise l'accessibilitĂ© et garantit que vos tests sont rĂ©silients aux changements dans la structure du DOM sous-jacente.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Cette requĂȘte trouve un Ă©lĂ©ment par le texte de son label associĂ©. C'est utile pour tester les Ă©lĂ©ments de formulaire.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Cette requĂȘte trouve un Ă©lĂ©ment par son texte de substitution (placeholder).
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Cette requĂȘte trouve un Ă©lĂ©ment image par son texte alternatif (alt text). Il est important de fournir un texte alternatif significatif pour toutes les images afin de garantir l'accessibilitĂ©.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Cette requĂȘte trouve un Ă©lĂ©ment par son attribut `title`.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Cette requĂȘte trouve un Ă©lĂ©ment par sa valeur affichĂ©e. C'est utile pour tester les champs de formulaire avec des valeurs prĂ©-remplies.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
RequĂȘtes `getAllBy*`
En plus des requĂȘtes `getBy*`, RTL fournit Ă©galement des requĂȘtes `getAllBy*`, qui retournent un tableau d'Ă©lĂ©ments correspondants. Celles-ci sont utiles lorsque vous devez affirmer que plusieurs Ă©lĂ©ments avec les mĂȘmes caractĂ©ristiques sont prĂ©sents dans le DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
RequĂȘtes `queryBy*`
Les requĂȘtes `queryBy*` sont similaires aux requĂȘtes `getBy*`, mais elles retournent `null` si aucun Ă©lĂ©ment correspondant n'est trouvĂ©, au lieu de lever une erreur. C'est utile lorsque vous voulez affirmer qu'un Ă©lĂ©ment n'est *pas* prĂ©sent dans le DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
RequĂȘtes `findBy*`
Les requĂȘtes `findBy*` sont des versions asynchrones des requĂȘtes `getBy*`. Elles retournent une Promesse (Promise) qui se rĂ©sout lorsque l'Ă©lĂ©ment correspondant est trouvĂ©. Elles sont utiles pour tester des opĂ©rations asynchrones, comme la rĂ©cupĂ©ration de donnĂ©es depuis une API.
// Simulation d'une récupération de données asynchrone
const fetchData = () => new Promise(resolve => {
setTimeout(() => resolve('Data Loaded!'), 1000);
});
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
test('charge les données de maniÚre asynchrone', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Data Loaded!');
expect(dataElement).toBeInTheDocument();
});
Simulation des interactions utilisateur
RTL fournit les API `fireEvent` et `userEvent` pour simuler les interactions de l'utilisateur, telles que cliquer sur des boutons, taper dans des champs de saisie et soumettre des formulaires.
`fireEvent`
`fireEvent` vous permet de déclencher des événements DOM par programmation. C'est une API de plus bas niveau qui vous donne un contrÎle précis sur les événements qui sont déclenchés.
<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';
test('simule un clic de bouton', () => {
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(alertMock).toHaveBeenCalledTimes(1);
alertMock.mockRestore();
});
`userEvent`
`userEvent` est une API de plus haut niveau qui simule les interactions de l'utilisateur de maniÚre plus réaliste. Elle gÚre des détails tels que la gestion du focus et l'ordre des événements, rendant vos tests plus robustes et moins fragiles.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('simule la saisie dans un champ de texte', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hello, world!');
expect(inputElement).toHaveValue('Hello, world!');
});
Tester le code asynchrone
De nombreuses applications React impliquent des opérations asynchrones, telles que la récupération de données depuis une API. RTL fournit plusieurs outils pour tester le code asynchrone.
`waitFor`
`waitFor` vous permet d'attendre qu'une condition devienne vraie avant de faire une assertion. C'est utile pour tester des opérations asynchrones qui prennent un certain temps à se terminer.
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
setTimeout(() => {
setData('Data loaded!');
}, 1000);
}, []);
return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';
test('attend que les données soient chargées', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Data loaded!'));
const dataElement = screen.getByText('Data loaded!');
expect(dataElement).toBeInTheDocument();
});
RequĂȘtes `findBy*`
Comme mentionnĂ© prĂ©cĂ©demment, les requĂȘtes `findBy*` sont asynchrones et retournent une Promesse qui se rĂ©sout lorsque l'Ă©lĂ©ment correspondant est trouvĂ©. Elles sont utiles pour tester des opĂ©rations asynchrones qui entraĂźnent des modifications du DOM.
Tester les Hooks
Les Hooks React sont des fonctions réutilisables qui encapsulent une logique avec état. RTL fournit l'utilitaire `renderHook` de `@testing-library/react-hooks` (qui est obsolÚte au profit de `@testing-library/react` directement depuis la v17) pour tester les Hooks personnalisés de maniÚre isolée.
// src/hooks/useCounter.js
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return { count, increment, decrement };
}
export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('incrémente le compteur', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Explication :
- `renderHook` : Cette fonction effectue le rendu du Hook et retourne un objet contenant le résultat du Hook.
- `act` : Cette fonction est utilisée pour envelopper tout code qui provoque des mises à jour d'état. Cela garantit que React peut correctement regrouper et traiter les mises à jour.
Techniques de test avancées
Une fois que vous maßtrisez les bases de RTL, vous pouvez explorer des techniques de test plus avancées pour améliorer la qualité et la maintenabilité de vos tests.
Mocker des modules
Parfois, vous devrez peut-ĂȘtre mocker des modules externes ou des dĂ©pendances pour isoler vos composants et contrĂŽler leur comportement pendant les tests. Jest fournit une API de mocking puissante Ă cet effet.
// src/api/dataService.js
export const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';
jest.mock('../api/dataService');
test('récupÚre les données depuis l\'API', async () => {
dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Mocked data!'));
expect(screen.getByText('Mocked data!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
Explication :
- `jest.mock('../api/dataService')` : Cette ligne mocke le module `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })` : Cette ligne configure la fonction `fetchData` mockée pour qu'elle retourne une Promesse qui se résout avec les données spécifiées.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)` : Cette ligne affirme que la fonction `fetchData` mockée a été appelée une fois.
Fournisseurs de contexte (Context Providers)
Si votre composant dépend d'un Fournisseur de contexte, vous devrez envelopper votre composant dans le fournisseur pendant les tests. Cela garantit que le composant a accÚs aux valeurs du contexte.
// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';
test('change le thĂšme', () => {
render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
const themeParagraph = screen.getByText(/Current theme: light/i);
const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});
Explication :
- Nous enveloppons le `MyComponent` dans `ThemeProvider` pour fournir le contexte nécessaire pendant les tests.
Tester avec le Router
Lorsque vous testez des composants qui utilisent React Router, vous devrez fournir un contexte de Router mocké. Vous pouvez y parvenir en utilisant le composant `MemoryRouter` de `react-router-dom`.
// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';
function MyComponent() {
return (
<div>
<Link to="/about">Go to About Page</Link>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';
test('affiche un lien vers la page \"Ă propos\"', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
Explication :
- Nous enveloppons le `MyComponent` dans `MemoryRouter` pour fournir un contexte de Router mocké.
- Nous affirmons que l'élément de lien a le bon attribut `href`.
Meilleures pratiques pour écrire des tests efficaces
Voici quelques meilleures pratiques à suivre lors de l'écriture de tests avec RTL :
- Concentrez-vous sur les interactions utilisateur : Ăcrivez des tests qui simulent la façon dont les utilisateurs interagissent avec votre application.
- Ăvitez de tester les dĂ©tails d'implĂ©mentation : Ne testez pas le fonctionnement interne de vos composants. Concentrez-vous plutĂŽt sur le comportement observable.
- Ăcrivez des tests clairs et concis : Rendez vos tests faciles Ă comprendre et Ă maintenir.
- Utilisez des noms de test significatifs : Choisissez des noms de test qui décrivent avec précision le comportement testé.
- Gardez les tests isolĂ©s : Ăvitez les dĂ©pendances entre les tests. Chaque test doit ĂȘtre indĂ©pendant et autonome.
- Testez les cas limites : Ne vous contentez pas de tester le "happy path". Assurez-vous de tester également les cas limites et les conditions d'erreur.
- Ăcrivez les tests avant de coder : Envisagez d'utiliser le dĂ©veloppement pilotĂ© par les tests (TDD) pour Ă©crire les tests avant d'Ă©crire votre code.
- Suivez le modĂšle "AAA" : Arrange, Act, Assert (Organiser, Agir, Affirmer). Ce modĂšle aide Ă structurer vos tests et Ă les rendre plus lisibles.
- Gardez vos tests rapides : Des tests lents peuvent dĂ©courager les dĂ©veloppeurs de les exĂ©cuter frĂ©quemment. Optimisez la vitesse de vos tests en mockant les requĂȘtes rĂ©seau et en minimisant la quantitĂ© de manipulation du DOM.
- Utilisez des messages d'erreur descriptifs : Lorsque les assertions échouent, les messages d'erreur doivent fournir suffisamment d'informations pour identifier rapidement la cause de l'échec.
Conclusion
React Testing Library est un outil puissant pour écrire des tests efficaces, maintenables et centrés sur l'utilisateur pour vos applications React. En suivant les principes et les techniques décrits dans ce guide, vous pouvez créer des applications robustes et fiables qui répondent aux besoins de vos utilisateurs. N'oubliez pas de vous concentrer sur les tests du point de vue de l'utilisateur, d'éviter de tester les détails d'implémentation et d'écrire des tests clairs et concis. En adoptant RTL et les meilleures pratiques, vous pouvez améliorer considérablement la qualité et la maintenabilité de vos projets React, quel que soit votre emplacement ou les exigences spécifiques de votre public mondial.